import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;

@SuppressWarnings("unused")
public class LoadProperties {
    @FunctionalInterface
    private interface InnerFunction<T> {
        T run(Object value);
    }
    private final Map<Class<?>, Object> objectMap = new HashMap<>();
    private final List<Class<?>> clsList = new ArrayList<>();
    private final Map<Class<?>, InnerFunction<?>> changeTable = new HashMap<>();
    LoadProperties(Class<?> baseClass) {
        try {
            changeTable.put(int.class, (InnerFunction<Integer>) value -> Integer.parseInt(value.toString()));
            changeTable.put(long.class, (InnerFunction<Long>) value -> Long.parseLong(value.toString()));
            changeTable.put(short.class, (InnerFunction<Short>) value -> Short.parseShort(value.toString()));
            changeTable.put(byte.class, (InnerFunction<Byte>) value -> Byte.parseByte(value.toString()));
            changeTable.put(boolean.class, (InnerFunction<Boolean>) value -> Boolean.parseBoolean(value.toString()));
            changeTable.put(char.class, (InnerFunction<Character>) value -> value.toString().charAt(0));
            changeTable.put(String.class, (InnerFunction<String>) Object::toString);

            var resource = baseClass.getResource("");
            if (resource != null) {
                var baseFile = new File(resource.toURI());
                var baseClsNames = baseClass.getName().split("\\.");
                var baseClsNameBuilder = new StringBuilder();
                for (int i = 0; i < baseClsNames.length - 1; i++) {
                    baseClsNameBuilder.append(baseClsNames[i]);
                    if (i != baseClsNames.length - 2) {
                        baseClsNameBuilder.append(".");
                    }
                }
                Queue<Pair<File, String>> fileQueue = new LinkedList<>();
                fileQueue.add(new Pair<>(baseFile, baseClsNameBuilder.toString()));
                while (!fileQueue.isEmpty()) {
                    var pair = fileQueue.remove();
                    var children = pair.key.listFiles();
                    if (children != null) {
                        for (var child : children) {
                            if (child.isDirectory()) {
                                fileQueue.add(new Pair<>(child, pair.value + "." + child.getName()));
                            } else {
                                var name = child.getName();
                                if (name.contains(".")) {
                                    var temp = name.split("\\.");
                                    if (temp[1].equals("class")) {
                                        var ans = new StringBuilder();
                                        ans.append(pair.value);
                                        if (!pair.value.isEmpty()) {
                                            ans.append(".");
                                        }
                                        ans.append(temp[0]);
                                        var c = Class.forName(ans.toString());
                                        if (!c.isInterface()) {
                                            clsList.add(Class.forName(ans.toString()));
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (URISyntaxException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public void setTable(Class<?> cls, InnerFunction<?> function) {
        if (changeTable.containsKey(cls)) {
            changeTable.replace(cls, function);
        } else {
            changeTable.put(cls, function);
        }
    }
    public void load(String... filePaths) throws FileNotFoundException {
        for (var filePath : filePaths) {
            var file = new File(filePath);
            if (file.exists() && file.isFile()) {
                if (filePath.contains("/")) {
                    var temp = filePath.split("/");
                    filePath = temp[temp.length - 1];
                }
                if (filePath.contains(".")) {
                    var suffix = filePath.split("\\.")[1];
                    //noinspection SwitchStatementWithTooFewBranches
                    switch (suffix) {
                        case "prop" -> setProp(file);
                    }
                }
            } else {
                throw new FileNotFoundException(filePath + " cannot be founded");
            }
        }
    }

    public void load(URI... uris) throws FileNotFoundException {
        for (var uri : uris) {
            load(uri.normalize().getPath());
        }
    }

    public void load(URL... urls) throws FileNotFoundException {
        for (var url : urls) {
            try {
                load(url.toURI());
            } catch (URISyntaxException e) {
                System.err.println("load(urls): " + e.getMessage());
            }
        }
    }

    public Object get(Class<?> cls) {
        return objectMap.get(cls);
    }

    private void setProp(File file) {
        var properties = new Properties();
        try (var reader = new FileReader(file, StandardCharsets.UTF_8)) {
            properties.load(reader);
            var map = new HashMap<>(properties);
            simpleSet(map);
        } catch (IOException e) {
            System.err.println("setProp(File file): " + e.getMessage());
        }
    }

    private void simpleSet(Map<Object, Object> map) {
        for (var cls : clsList) {
            if (!cls.isAnnotationPresent(Value.class)) {
                continue;
            }
            var temp = cls.getConstructors();
            if (temp.length == 0) {
                continue;
            }
            var constructor = temp[0];
            var paras = constructor.getParameters();
            List<Object> paraValues = new ArrayList<>();
            for (var para : paras) {
                if (para.isAnnotationPresent(Value.class)) {
                    var key = para.getAnnotation(Value.class).value();
                    paraValues.add(changeTable.get(para.getType()).run(map.get(key)));
                }
            }
            try {
                Object obj;
                obj = constructor.newInstance(paraValues.toArray());
                var fields = cls.getFields();
                for (var field : fields) {
                    if (field.isAnnotationPresent(Value.class)) {
                        var key = field.getAnnotation(Value.class).value();
                        field.set(obj, changeTable.get(field.getType()).run(map.get(key)));
                    }
                }
                var methods = cls.getMethods();
                for (var method : methods) {
                    if (method.isAnnotationPresent(Value.class) && method.getName().startsWith("set")) {
                        var key = method.getAnnotation(Value.class).value();
                        method.invoke(obj, changeTable.get(method.getReturnType()).run(map.get(key)));
                    }
                }
                objectMap.put(cls, obj);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                System.err.println("simpleSet(Set<Map.Entry<Object, Object>> mapSet): " + e.getMessage());
            }
        }
    }
}